17 向量和 SIMD 简介
在本章中,我想讨论 Zig 中的向量,它们与 SIMD 操作相关(即,它们与std::vector
C++ 中的类没有关系)。
17.1什么是SIMD?
SIMD(单指令/多数据)是一组广泛应用于视频/音频编辑程序以及图形应用程序的操作。SIMD 并非一项新技术,但在普通台式计算机上大规模使用 SIMD 才刚刚开始。过去,SIMD 仅用于“超级计算机”。
如今,大多数现代 CPU(包括 AMD、Intel 等品牌的 CPU)(无论是台式机还是笔记本电脑)都支持 SIMD 操作。因此,如果您的电脑中安装了非常老旧的 CPU,则可能不支持 SIMD 操作。
人们为什么开始在软件中使用 SIMD?答案是为了提高性能。但是,SIMD 究竟是如何实现更佳性能的呢?本质上,SIMD 操作是一种在程序中进行并行计算的策略,从而提高计算速度。
SIMD 背后的基本思想是用一条指令同时对多个数据进行操作。执行普通标量运算时,例如四条加法指令,每个加法指令都是单独执行的,一个接一个。但使用 SIMD 时,这四条加法指令会被转换为一条指令,因此,这四条加法指令可以同时并行执行。
目前,zig
编译器允许您对向量对象应用以下一组运算符。当您对向量对象应用这些运算符之一时,将使用 SIMD 进行计算,因此,默认情况下,这些运算符将逐元素并行应用。
- 算术(
+
,,,,,,,,等)。-
/``*``@divFloor()``@sqrt()``@ceil()``@log()
- 位运算符(
>>
,,,,,<<
等)。&``|``~
- 比较运算符(
<
,,,等)>
。==
17.2向量
SIMD 操作通常通过_SIMD 内部函数 (intrinsic)_执行,这只是执行 SIMD 操作的函数的别称。这些 SIMD 内部函数(或“SIMD 函数”)始终作用于一种特殊类型的对象,这种对象被称为“向量”。因此,要使用 SIMD,您必须创建一个“向量对象”。
向量对象通常是一个固定大小的 128 位(16 字节)块。因此,您在实际中发现的大多数向量本质上都是数组,包含 2 个 8 字节的值,或者 4 个 4 字节的值,或者 8 个 2 字节的值,等等。但是,不同的 CPU 型号可能具有不同的 SIMD 扩展(或“实现”),它们可能提供更多类型、更大尺寸(256 位或 512 位)的向量对象,以便在单个向量对象中容纳更多数据。
您可以使用@Vector()
内置函数在 Zig 中创建一个新的向量对象。在此函数中,您可以指定向量长度(向量中元素的数量)以及向量元素的数据类型。这些向量对象仅支持原始数据类型。在下面的示例中,我创建了两个向量对象(v1
和v2
),每个对象包含 4 个元素u32
。
另请注意,在下面的示例中,第三个向量对象(v3
)是由前两个向量对象(v1
plus v2
)的和创建的。因此,对向量对象的数学运算默认按元素进行,因为相同的运算(在本例中为加法)被转换为单个指令,并在向量的所有元素上并行复制。
const v1 = @Vector(4, u32){4, 12, 37, 9};
const v2 = @Vector(4, u32){10, 22, 5, 12};
const v3 = v1 + v2;
try stdout.print("{any}\n", .{v3});
{ 14, 34, 42, 21 }
这就是 SIMD 提升程序性能的方式。我们无需使用 for 循环遍历v1
和的元素v2
,然后一次一个元素地将它们相加,而是可以享受 SIMD 的优势,它可以同时并行执行所有 4 个加法运算。
因此,该@Vector
结构本质上是 SIMD 矢量对象的 Zig 表示。当且仅当您当前的 CPU 型号支持 SIMD 操作时,这些矢量对象中的元素才会并行操作。如果您的 CPU 型号不支持 SIMD,那么该@Vector
结构可能会产生与“for 循环解决方案”类似的性能。
17.2.1将数组转换为向量
将普通数组转换为矢量对象有多种方法。您可以使用隐式转换(即将数组直接赋值给矢量对象),也可以使用切片从普通数组创建矢量对象。
在下面的例子中,我们隐式地将数组转换a1
为长度为 4 的向量对象(v1
)。我们首先明确注释向量对象的数据类型,然后将数组对象分配给这个向量对象。
还要注意,在下面的例子中,第二个向量对象(v2
)也是通过获取数组对象()的切片a1
,然后将指向该切片的指针(.*
)存储到该向量对象中来创建的。
const a1 = [4]u32{4, 12, 37, 9};
const v1: @Vector(4, u32) = a1;
const v2: @Vector(2, u32) = a1[1..3].*;
_ = v1; _ = v2;
值得强调的是,只有编译时大小已知的数组和切片才能转换为向量。向量通常是一种仅在编译时大小已知的情况下才能工作的结构。因此,如果您有一个运行时大小已知的数组,那么在将其转换为向量之前,您需要先将其复制到一个编译时大小已知的数组中。
17.2.2函数@splat()
您可以使用@splat()
内置函数创建一个向量对象,该对象的所有元素都填充相同的值。此函数旨在提供一种快速简便的方法,将标量值(即单个值,例如单个字符或单个整数等)直接转换为向量对象。
因此,我们可以用@splat()
它将单个值(例如整数)转换16
为长度为 1 的向量对象。但我们也可以使用此函数将同一个整数转换16
为长度为 10 的向量对象,即填充 10 个16
值。下面的示例演示了这个想法。
const v1: @Vector(10, u32) = @splat(16);
try stdout.print("{any}\n", .{v1});
{ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }
17.2.3小心过大的向量
正如我在17.2 节中所述,每个向量对象通常都是 128、256 或 512 位的小块。这意味着向量对象通常很小,而当你试图反其道而行之时,通过创建一个非常大的向量对象(即,大小接近220),通常会导致编译器崩溃和出现严重错误。
例如,如果您尝试编译下面的程序,则可能会在构建过程中遇到段错误或 LLVM 错误。请注意不要创建过大的向量对象。
const v1: @Vector(1000000, u32) = @splat(16);
_ = v1;
Segmentation fault (core dumped)